package breadbox.dataManagement;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.rmi.UnmarshalException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import breadbox.client.operations.OperationType;
import breadbox.pastry.Application;
import breadbox.pastry.ID;
import breadbox.pastry.LongID;
import breadbox.pastry.Message;
import breadbox.pastry.Node;
import breadbox.pastry.PastryNode;
import breadbox.pastry.routing.LeafSet;

public class NetworkedManager extends Manager implements Application, RMIMessage {

	protected Node pastry; 
	protected LeafSet leafset;
	//the lock that is associated with updating the leafset and replication
	protected Object leafsetLock;
	protected NetworkedReplicationManager networkedRepManager;
	public NetworkedManager() throws IOException {
		super();
		leafsetLock = new Object();
		storage = new DiskStorage("breadboxStorage");
		repManager = new NetworkedReplicationManager(this);
		pastry = PastryNode.getPastryNode();
		pastry.register(this);
		pastry.open();
		pastry.join();
		networkedRepManager = (NetworkedReplicationManager)repManager;
		networkedRepManager.setCurrentNode(pastry.getNodeId());
	}
	public static void main(String[] args) throws Exception{
			
		Manager m = new NetworkedManager();
		m.startRMI();
		//m.startReplication();
	}
	
	public void replicate(byte[] d,ID to,boolean b) {
		if(replicate) {
			networkedRepManager.replicate(d,to,b);
		}
	}
	
	public void replicateDelete(ID to) {
		if(replicate) {
			networkedRepManager.replicateDelete(to);
		}
	}
	 
	//this method is called by the client to get,put,or delete a chunk
	//the message is passed to pastry
	public void sendMessage(Message m) {
		OperationType key = OperationType.valueOf(m.key);
		if(key == OperationType.INITIALIZE) {
			initializeClient(m);
		}
		//check if the chunk exists locally and message type is a GET
		else if(storage.exists(m.to) && key == OperationType.GET) {
			Message returnMessage;
			//retrieve chunk from storage system and don't send it through pastry
			try {
				try {
					byte[] data = storage.get(m.to);
					//the id doesn't matter since it is retrieved locally
					returnMessage = new Message(0, "GET_RESPONSE", data);
				}
				catch(Exception e) {
					returnMessage = new Message(0,"GET_ERROR",null);
				}
				client.sendMessage(returnMessage);
			}
			catch(Exception e) {
				//e.printStackTrace();
			}
		}
		else {
			int err = route(m);
			if(err != 0) {	//TODO: I don't know what the error codes are
				//do something
			}
		}
	}
	
	
	public int route(Message m) {
		return pastry.route(m);
	}
	
	public void add(ID id, byte[] d, boolean b) throws IOException {
		storage.write(id, d);
		if(networkedRepManager.isTreeInitialized()) {
			networkedRepManager.addIDToTree(id);
		}
		replicate(d, id, b);
	}
	
	public void delete(ID id, boolean b) throws IOException{
		storage.delete(id);
		if(networkedRepManager.isTreeInitialized()) {
			networkedRepManager.deleteIDFromTree(id);
		}
		if(b) {
			replicateDelete(id);
		}
	}
	
	//This does a ping
	public int proximity(InetAddress ip, int port) {
		Process ping;
		boolean isWindows = false;
		String os = System.getProperty("os.name");
		if(os.startsWith("Windows")) {
			isWindows = true;
		}
		try {
			if(isWindows) {
				ping = Runtime.getRuntime().exec("ping -w 999 -n 3 " + ip.getHostAddress());
			}
			//this should work for linux and OSX
			else {
				ping = Runtime.getRuntime().exec("ping -c3 -W1 " + ip.getHostAddress());
			}
		}
		catch(Exception e) {
			return 500;
		}
		InputStream traceOutputStream = ping.getInputStream();
		ByteArrayOutputStream traceOutputByteArray = new ByteArrayOutputStream();
		try {
			byte[] buffer;
			int num = 0;
			while(true) {
				buffer = new byte[256];
				num = traceOutputStream.read(buffer);
				if(num == -1) {
					break;
				}
				traceOutputByteArray.write(buffer,0,num);
			}
			String str = traceOutputByteArray.toString();
			
			Pattern p;
			if(isWindows) {
				p = Pattern.compile("Average = (\\d+)");
			}
			else {
				p = Pattern.compile("\\d+\\.\\d+/(\\d+)\\.\\d+");
			}
			Matcher m = p.matcher(str);
			if(m.find()) {
				return Integer.parseInt(m.group(1));
			}
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		return 500;
	}
	
	public void deliver( Message m ) {
		OperationType t = OperationType.valueOf(m.key);
		int err;
		switch(t) {
			case PUT: {
				try {
					byte[] d = m.data;
					add(m.to, d, true);
				}
				catch(Exception e) {
					e.printStackTrace();
				}
				break;
			}
			case DELETE: {
				//delete the chunk, don't send anything to client or pastry
				try {
					delete(m.to, true);
				}
				catch(Exception e) { }
				break;
			}
			case GET: {
				//TODO: keep track of commonly accessed chunks
				Message returnMessage;
				//retrieve chunk from storage system and send it through pastry
				try {
					long returnID;
					if (m.from != null) {
						returnID = Long.parseLong(m.from.toString(10));
					} else {
						returnID = 0;
					}
					try {
						byte[] data = storage.get(m.to);
						returnMessage = new Message(returnID, "GET_RESPONSE", data);
					}
					catch(Exception e) {
						returnMessage = new Message(returnID, "GET_ERROR", null);
					}
					err = route(returnMessage);
				}
				catch(NumberFormatException n) {
					System.err.println("invalid from ID, cannot send message");
				}
				break;
			}
			case GET_RESPONSE: {
				//pass message to client
				try {
					if(client == null) {
						System.err.println("client is null for an unknown reason");
					}
					else {
						client.sendMessage(m);
					}
				} catch(UnmarshalException e1) { 
					// do nothing, client finished
				} catch(Exception e) {
					e.printStackTrace();
				}
				break;
			}
			case GET_ERROR: {
				//pass message to client
				try {
					client.sendMessage(m);
				}
				catch(Exception e) {
					e.printStackTrace();
				}
				break;
			}
			case REPLICATE_INITIAL_PUT: {
				try {
					ReplicationChunk r = new ReplicationChunk(m.data);
					byte[] d = r.getData();
					//write this replicated chunk using its chunkID, not NodeID
					add(new LongID(r.getID()), d, false);
				}
				catch(Exception e) {
					//e.printStackTrace();
				}
				break;
			}
			case REPLICATE_PUT: {
				try {
					ReplicationChunk r = new ReplicationChunk(m.data);
					if(!storage.exists(new LongID(r.getID()))) {
						Message returnMessage = new Message(r.getID(), "REPLICATE_GET", null);
						route(returnMessage);
					}
					else {
						storage.touch(new LongID(r.getID()));
					}
				}
				catch(Exception e) { }
				break;
			}
			case REPLICATE_GET: {
				//same as GET but the message response type is different
				Message returnMessage;
				//retrieve chunk from storage system and send it through pastry
				try {
					long returnID = Long.parseLong(m.from.toString(10));
					try {
						byte[] data = storage.get(m.to);
						returnMessage = new Message(returnID, "REPLICATE_GET_RESPONSE", data);
					}
					catch(Exception e) {
						returnMessage = new Message(returnID,"REPLICATE_ERROR",null);
					}
					err = route(returnMessage);
				}
				catch(NumberFormatException n) {
					System.err.println("invalid from ID, cannot send message");
				}
				break;
			}
			case REPLICATE_GET_RESPONSE: {
				//similar to a GET_RESPONSE except the data is stored locally instead of
				//transferred to the client
				try {
					byte[] d = m.data;
					add(m.to, d, false);
				}
				catch(Exception e) {
					e.printStackTrace();
				}
				break;
			}
			case REPLICATE_DELETE: {
				try {
					ReplicationChunk r = new ReplicationChunk(m.data);
					delete(new LongID(r.getID()),false);
				}
				catch (Exception e) { }
				break;
			}
			case REPLICATE_ERROR: {
				break;
			}
			default: 
				return;
		}
	}
	
	public void newLeaf( LeafSet l ) {
		synchronized(leafsetLock) {
			leafset = l;
		}
		if(replicate) {
			((NetworkedReplicationManager)repManager).updateRootChunks(leafset);
		}
	}
	
	//not implemented
	public void forward( Message m ) {
		
	}
	
	public LeafSet getLeafSet() {
		return leafset;
	}
	
	public Object getLeafSetLock() {
		return leafsetLock;
	}
	
	
	//function to test writing to a storage Object
	public void testWrite(long decID) {
		System.out.println("testing write");
		byte[] temp = new byte[1024];
		for(int i=0; i<1024; i++) {
			temp[i] = (byte)i;
		}
		ID id = new LongID(decID);
		try {
			storage.write(id, temp);
		}
		catch(Exception e) {
			System.out.println(e.toString());
			 e.printStackTrace();
		}
		
	}
	
	//function to test reading to a storage Object
	public void testRead(long decID) {
		System.out.println("testing read");
		byte[] temp = null;
		try {
			ID id = new LongID(decID);
			temp = storage.get(id);
		}
		catch(Exception e) {
			System.out.println(e.toString());
			 e.printStackTrace();
		}
		if(temp == null) {
			System.out.println("returned null");
		}
		if(temp.length != 1024) {
			System.out.println("not correct length");
		}
		
		boolean bad = false;
		for(int i=0; i<1024; i++) {
			if(temp[i] != (byte)i) {
				bad = true;
			}
		}
		if(bad) {
			System.out.println("bad");
		}
	}
}
